Skip to content

feat: classify retryable transaction exceptions#10162

Open
memleakd wants to merge 3 commits intocodeigniter4:4.8from
memleakd:feat/retryable-transaction-exceptions
Open

feat: classify retryable transaction exceptions#10162
memleakd wants to merge 3 commits intocodeigniter4:4.8from
memleakd:feat/retryable-transaction-exceptions

Conversation

@memleakd
Copy link
Copy Markdown
Contributor

@memleakd memleakd commented May 6, 2026

Description

This PR proposes adding RetryableTransactionException for driver-specific transaction failures that are reasonable candidates for retrying the whole transaction, such as deadlocks and serialization failures.

The goal is to give CodeIgniter a small, conservative, driver-aware classification point without adding another public helper method to database connections. This follows the same direction as UniqueConstraintViolationException: when the driver recognizes a known database condition, it can throw a more specific exception type instead of a plain DatabaseException.

This can already help applications that want to implement their own retry policy:

try {
    $orderId = $db->transException(true)->transaction(static function ($db) use ($order) {
        $db->table('orders')->insert($order);

        return $db->insertID();
    });
} catch (RetryableTransactionException $e) {
    // Retry the whole transaction according to the application's policy.
    throw $e;
}

It is also intended as groundwork for a possible follow-up proposal to add opt-in retry support to the closure-based transaction() helper. Keeping the exception classification separate makes that later discussion smaller: retryable database errors can be reviewed independently from retry control flow, attempt counts, backoff behavior, and callback side-effect concerns.

This PR does not retry anything automatically. It only classifies known retryable transaction failures through a dedicated exception type.

Changes

  • Added RetryableTransactionException, extending DatabaseException.
  • Added centralized semantic exception creation in BaseConnection.
  • Routed unique constraint and retryable transaction classification through the shared database exception factory.
  • Added driver-specific retryable transaction classification.
  • Simplified driver query execution paths to use the shared exception factory.
  • Added focused tests for retryable, non-retryable, and unique-constraint exception classification.
  • Updated the transaction docs and v4.8.0 changelog.

Driver Policy

The retryable classification is intentionally conservative. It includes transaction-level concurrency failures where retrying the whole transaction is a reasonable default:

  • MySQLi deadlock: 1213
  • Postgre serialization failure/deadlock: 40001, 40P01
  • SQLite busy: 5
  • SQLSRV deadlock/snapshot conflict: 1205, 3960
  • OCI8 deadlock/serialization failure: 60, 8177

It intentionally does not classify lock timeouts, unique constraint violations, resource-busy errors, or SQLite extended busy codes as framework-default retryable transaction failures. Those can be valid retry cases in some applications, but they are more policy-dependent.

References: MySQL, PostgreSQL, SQLite, SQL Server deadlocks, SQL Server 3960, Oracle ORA-00060, Oracle ORA-08177.

Scope Note

This PR focuses on the normal driver query execution paths. Prepared queries still have separate exception paths today, similar to the existing unique-constraint behavior. I kept that out of this PR so it stays focused, but I’m happy to work on prepared-query exception consistency as a follow-up.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@github-actions github-actions Bot added the 4.8 PRs that target the `4.8` branch. label May 6, 2026
@michalsn
Copy link
Copy Markdown
Member

michalsn commented May 7, 2026

Thanks for working on this. The goal makes sense. It's useful to flag transaction errors that are worth retrying, like deadlocks and serialization failures. However, I wonder if this should follow the same approach we recently added with UniqueConstraintViolationException in #9979.

Instead of adding a public method like $db->isRetryableTransactionException($e) could the drivers throw a clearer exception type? For example:

try {
    $result = $db->transException(true)->transaction(static function ($db) {
        // ...
    });
} catch (RetryableTransactionException $e) {
    // Retry the whole transaction
}

This seems doable because the drivers already inspect native database codes when creating UniqueConstraintViolationException. The same place could classify deadlocks or serialization failures and throw RetryableTransactionException instead of a plain DatabaseException.

Overall, this looks useful, especially the conservative list of retryable codes. My main suggestion is about the public API shape. I would prefer named exception over isRetryableTransactionException().

- Replace the public retryable transaction classifier with a named exception
- Classify retryable driver errors through the database exception factory
- Update docs and tests for exception-based retry handling

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@memleakd
Copy link
Copy Markdown
Contributor Author

memleakd commented May 7, 2026

Thanks a lot for pointing this out. That makes total sense, and I agree it's a better direction. I missed the previous UniqueConstraintViolationException work, so thank you for connecting it here.

I refactored the PR around that idea. The drivers now classify the known retryable transaction errors and throw RetryableTransactionException, and the docs/tests were updated to show the exception-based usage.

One thing I noticed while doing this is that prepared queries still have separate exception paths, similar to the existing unique-constraint behavior. I kept that out of this PR so it stays focused, but I'd be happy to work on prepared-query consistency as a follow-up after this one.

Comment thread system/Database/MySQLi/Connection.php Outdated
@patel-vansh
Copy link
Copy Markdown
Contributor

Thanks a lot for pointing this out. That makes total sense, and I agree it's a better direction. I missed the previous UniqueConstraintViolationException work, so thank you for connecting it here.

I refactored the PR around that idea. The drivers now classify the known retryable transaction errors and throw RetryableTransactionException, and the docs/tests were updated to show the exception-based usage.

One thing I noticed while doing this is that prepared queries still have separate exception paths, similar to the existing unique-constraint behavior. I kept that out of this PR so it stays focused, but I'd be happy to work on prepared-query consistency as a follow-up after this one.

Also, please update the PR description as well.

- Route unique constraint and retryable transaction errors through the database exception factory
- Move driver-specific unique constraint checks into connection overrides
- Add factory coverage for unique constraint exception classification

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants